Demand Response Optimization for Battery Energy Storage#679
Demand Response Optimization for Battery Energy Storage#679vijay092 wants to merge 7 commits intoNatLabRockies:developfrom
Conversation
elenya-grant
left a comment
There was a problem hiding this comment.
Hi Sanjana! Just putting a few high-level comments now - but I will do a deeper dive shortly! I love the thorough testing of PLMOptimizedControllerConfig and PLMOptimizedStorageController! The docstrings also look awesome! Besides the comments and questions that I left, other high-level things I'd like to see included before this is merged in are:
- integration tests of the controller with the
StoragePerformanceModel- similar to the tests inh2integrate/control/control_strategies/storage/test/test_optimal_controllers.py - a test for the example you added in
examples/test/test_all_examples.py(this can be a pretty basic test, but it'll be good to ensure that the example you added is updated and runs without error as H2I is further developed)
Truly awesome work! Please message or call me if you want to discuss any of the comments I left! I will do another review that focuses on the logic within the controller - but wanted to get these initial questions and comments to you early on!
| discharge_efficiency: float = field(validator=range_val(0, 1), default=1.0) | ||
| n_max_events: int = field(default=10) | ||
| n_control_window: int = field(default=24 * 30) # one month of hourly data | ||
| signal_threshold_percentile: float = field(default=0.0) |
There was a problem hiding this comment.
Small comment - may be nice to add a range_val validator for the signal_threshold_percent (unless it can be over 100)?
| signal_threshold_percentile: float = field(default=0.0) | |
| signal_threshold_percentile: float = field(default=0.0, validator=range_val(0,100)) |
This is a suggestion/thought/nitpick (but not necessary) - sometimes it's nice to use fraction instead of percentages (specifically with range_val validators) because it is easier to catch with validators.
Aka - if a parameter is defined as a percent (between 0 and 100), but a user thinks its defined as a fraction (suppose they want it to be 40%, but set the value to 0.4 because they think its a fraction), then this would not throw an error with a validator. But - if a parameter is defined as a fraction (between 0 and 1) and a user thinks its defined as a percentage (they want it to be 40% so they set the value to 40), then the validator does thrown an error.
There was a problem hiding this comment.
Typically, percentiles are not written in terms of fractions (unlike percentages). Have you seen this done before?
There was a problem hiding this comment.
Also, that sounds good about the range_val. I hadn't added it because np.percentile will catch it anyway.
There was a problem hiding this comment.
I think it would be worth including the validator even if np.percentile will catch it later because the validator will provide a very clear error message and appear readable for users. non-blocking comment.
There was a problem hiding this comment.
That sounds good, I added it.
|
|
||
| max_charge_rate: float = field() | ||
| supervisory_signal: list = field() | ||
| peak_window: dict = field() |
There was a problem hiding this comment.
Is there a limit/constraint on peak_window specifically with respect to n_control_window? Like what if someone defined n_control_window = 6 and peak_window["start"] = 12:00:00 and peak_window["end"] = 21:00:00 (peak window is 9 hours, meaning that peak_window>n_control_window).
Should there be a minimum value on n_control_window? (not necessary - just asking!)
There was a problem hiding this comment.
Just checking - can peak_window['end'] be before peak_window['start']? Like if I wanted to define my peak window from 11pm (23:00:00) to 3am (03:00:00) - would that work?
There was a problem hiding this comment.
No, there is no constraint on peak_window wrt n_control_window. n_control_window is just iterating over the 8760 is small chunks.
There was a problem hiding this comment.
_compute_peak_window_mask raises a ValueError if end < start.
| performance_incentive (float) | ||
| Incentive revenue ($/kW per dispatch hour). | ||
| n_max_events (int) | ||
| Maximum discharge events per control window. Default 10. |
There was a problem hiding this comment.
great doc-strings! These are so helpful!
| peak_window (dict) | ||
| Hours eligible for dispatch. 'start' and 'end' as HH:MM:SS strings. | ||
| performance_incentive (float) | ||
| Incentive revenue ($/kW per dispatch hour). |
There was a problem hiding this comment.
| Incentive revenue ($/kW per dispatch hour). | |
| Incentive revenue ($/commodity_rate_units per dispatch hour). |
There was a problem hiding this comment.
I've only thought about this PR as a battery use case. We should chat about what this would mean.
| m = pyomo.ConcreteModel(name="plm_dr") | ||
|
|
||
| # Parameters | ||
| P_max = self.config.max_charge_rate |
There was a problem hiding this comment.
In order for the controller to be compatible with a case where we want to optimize the storage charge rate or capacity, then any method called in compute() should use inputs["max_charge_rate"] instead of self.config.max_charge_rate and inputs["storage_capacity"] instead of self.config.max_capacity.
An example of how to test this is somewhat shown in h2integrate/storage/test/test_storage_performance_model.py::test_generic_storage_with_simple_control_charge_rate_lessthan_demand, using the prob.set_val function (happy to chat about it if you want)
b82857b to
410e488
Compare
jaredthomas68
left a comment
There was a problem hiding this comment.
This is great Sanjana! I'm excited about where this is going. I gave a lot of comments, but some of them may be irrelevant due to my experience with pyomo. The doc page details were very helpful, thank you.
| **Given:** | ||
| - $\lambda_t$ := `supervisory_signal`: price, demand, or price $\times$ demand time series at time $t$ | ||
| - $\mathcal{W}$ := `peak_window`: set of hours eligible for dispatch (e.g., 12:00--19:00) | ||
| - $\gamma$ := performance incentive (\$/kW per dispatch hour) |
There was a problem hiding this comment.
is this truly required to be per hour, or is it per time-step? In other words, could it be per-minute if the user requested a smaller time step?
There was a problem hiding this comment.
I was under the impression that H2I only supports hourly timesteps. I can make this more general.
There was a problem hiding this comment.
We are currently in the process of enabling smaller time steps, so we are building the capability into new additions to the code!
There was a problem hiding this comment.
Oh, that's awesome!
| - $\overline{\text{SoC}}$ := `max_soc_fraction`, $\quad \underline{\text{SoC}}$ := `min_soc_fraction` | ||
| - `n_control_window` := Horizon length for optimization | ||
| - $\mathcal{T} := \{0, 1, \ldots, T\}$: hourly time steps over `n_control_window` | ||
| - $\mathcal{M}_m$ := set of hours in month $m$, for $m = 1, \ldots, 12$ |
There was a problem hiding this comment.
Same as above, does this have to be hours?
| - $\eta_c$ := `charge_efficiency`, $\quad \eta_d$ := `discharge_efficiency` | ||
| - $\overline{\text{SoC}}$ := `max_soc_fraction`, $\quad \underline{\text{SoC}}$ := `min_soc_fraction` | ||
| - `n_control_window` := Horizon length for optimization | ||
| - $\mathcal{T} := \{0, 1, \ldots, T\}$: hourly time steps over `n_control_window` |
There was a problem hiding this comment.
Hours or time steps?
|
|
||
| - $u_t \in \{0, 1\}$ := discharge binary: 1 if battery dispatches at hour $t$, 0 otherwise | ||
| - $v_t \in \{0, 1\}$ := charge binary: 1 if battery charges at hour $t$, 0 otherwise | ||
|
|
There was a problem hiding this comment.
hours or time steps?
| Maximize total annual incentive revenue: | ||
|
|
||
| $$ | ||
| \max_{u_t,\, v_t} \quad \gamma \cdot \bar{P} \sum_{t \in \mathcal{T}} u_t |
There was a problem hiding this comment.
should the objective be based on max charge/discharge rate, or on actual discharge rate at each time step?
There was a problem hiding this comment.
I was assuming that the battery always charges and discharges at max discharge rate. If we want to solve for the actual discharge rate as well as when (the binary var) we want to discharge, the math becomes a little harder. I can work on that tomorrow.
| ) | ||
|
|
||
| m.objective = pyomo.Objective( | ||
| expr=-incentive * P_max * sum(m.discharge[t] for t in m.T), |
There was a problem hiding this comment.
is there a good way to use possible discharge instead of rating (see my similar comment in docs)
There was a problem hiding this comment.
Totally fair point, I am working on it.
| + eta_c * mdl.charge[t] * P_max / E_max | ||
| - mdl.discharge[t] * P_max / (eta_d * E_max) |
There was a problem hiding this comment.
see my comments in docs
| RuntimeError: If GLPK returns a non-OK status or an | ||
| unacceptable termination condition. | ||
| """ | ||
| from pyomo.opt import SolverStatus, TerminationCondition |
There was a problem hiding this comment.
why is the import statement inside the method? Can you move it to the header?
| pyomo.value(self.dr_model.objective), | ||
| ) | ||
|
|
||
| def compute(self, inputs, outputs, discrete_inputs, discrete_outputs): |
There was a problem hiding this comment.
I think it generally makes sense to keep the standard methods (init, setup, and compute) at the top of the class definition and have all other methods come after.
| list[float]: ``(u_t - v_t) * P_max`` for each timestep in | ||
| the solved window. Positive = discharge, negative = charge. | ||
| """ | ||
| P_max = self.config.max_charge_rate |
There was a problem hiding this comment.
I think P_max should be adjusted to not overcharge the battery, but still allow it to go to max charge. Maybe you have the handled somehow and I missed it.
3d4735f to
2f0a325
Compare
Demand Response Optimization for Battery Energy Storage (Stage 1)
This PR introduces a Pyomo-based formulation for demand response, which will be implemented in two stages.
As the first stage, this work implements a rolling horizon optimization for battery operations. The battery dispatch logic is based on a pre-defined signal, such as LMP, load, or a combination of both. This is the G&T level dispatch signal for demand response. The next stage will implement the peak load management logic.
Section 1: Type of Contribution
Section 2: Draft PR Checklist
TODO:
Type of Reviewer Feedback Requested (on Draft PR)
Structural feedback: Is this in the right place?
Implementation feedback: I used the same style as Gen's pyomo implementation. Appreciate feedback here.
Other feedback:
Section 3: General PR Checklist
docs/files are up-to-date, or added when necessaryCHANGELOG.md"A complete thought. [PR XYZ]((https://github.com/NatLabRockies/H2Integrate/pull/XYZ)", where
XYZshould be replaced with the actual number.Section 3: Related Issues
Section 4: Impacted Areas of the Software
Section 4.1: New Files
Main Implementation:
h2integrate/control/control_strategies/storage/plm_optimized_storage_controller.pyUsage Example:
examples/34_plm_optimized_dispatchSection 4.2: Modified Files
h2integrate/core/supported_models.pySection 5: Additional Supporting Information
Section 6: Test Results, if applicable
Section 7 (Optional): New Model Checklist
docs/developer_guide/coding_guidelines.mdattrsclass to define theConfigto load in attributes for the modelBaseConfigorCostModelBaseConfiginitialize()method,setup()method,compute()methodCostModelBaseClasssupported_models.pycreate_financial_modelinh2integrate_model.pytest_all_examples.pydocs/user_guide/model_overview.mddocs/section<model_name>.mdis added to the_toc.yml